<?php class HeatherSource {
	const SIGNATURE = 'I ARE AIRPORT CAT, THIS IS HEATHER THREAD';
	protected $_Strings = array ();
	protected $_Current = -1;
	protected $_Head = array ('Title' => '');
	protected $_EmptyCounter = 0;
	protected $_SavedIndentLevel = 0;
	protected $_CodeState = array ();

	// parser functions
	protected function CheckPrefix ($Prefix, $String) {
		if (is_scalar ($String) and is_scalar ($Prefix)) {
			return ($Prefix === substr ($String, 0, strlen ($Prefix)));
		} else return (FALSE);
	}

	// object functions
	protected function CodeStateOff () {
		$this->_CodeState = array ('Active' => FALSE, 'WSPrefix' => NULL, 'WSPrefixLen' => 0, 'Level' => 0, 'Operator' => '');
	}
	protected function CodeStateSetWSPrefix ($WSPrefix) {
		$this->_CodeState ['WSPrefix'] = $WSPrefix;
		$this->_CodeState ['WSPrefixLen'] = strlen ($this->_CodeState ['WSPrefix']);
	}
	protected function ConstructLine ($String) {
		if ($this->_CodeState ['Active']) { // maybe a code line
			if (HeatherString::IsEmpty ($String)) { // an empty line
				$this->_EmptyCounter += 1;
				if ($this->_EmptyCounter > 2) {
					$this->CodeStateOff ();
					return (new HeatherLineSpecial ('Empty', $String, $this->_SavedIndentLevel));
				}
			} else $this->_EmptyCounter = 0;
			if (NULL === $this->_CodeState ['WSPrefix']) { // no WS prefix recorded
				$Trimmed = ltrim ($String);
				$this->CodeStateSetWSPrefix (substr ($String, 0, strlen ($String) - strlen ($Trimmed)));
				return (new HeatherLineCode ($Trimmed, $this->_CodeState ['Level'], $this->_CodeState ['Operator']));
			} else { // WS Prefix exists
				if ($this->_CodeState ['WSPrefix'] === (string) substr ($String, 0, $this->_CodeState ['WSPrefixLen'])) { // WS prefix match
					return (new HeatherLineCode (substr ($String, $this->_CodeState ['WSPrefixLen']), $this->_CodeState ['Level'], $this->_CodeState ['Operator']));
				} else { // WS prefix mismatch, not a code line after all
					$this->CodeStateOff ();
				}
			}
		}
		if (HeatherString::IsEmpty ($String)) { // another empty line test, this time after the code tests
			return (new HeatherLineSpecial ('Empty', $String, $this->_SavedIndentLevel));
		}
		$Length = strlen ($String);
		if ($Length > 3 and str_pad ('', $Length, '=') === $String) return (new HeatherLineSpecial ('Section'));
		if ($Length > 3 and str_pad ('', $Length, '-') === $String) return (new HeatherLineSpecial ('Subsection'));
		$Level = $Operator = 0;
		$IndentLength = strspn ($String, ' *-');
		if ($IndentLength > 3) {
			if ($IndentLength % 2) { // odd number of spaces; let's pretend there's one more space
				$String = ' '.$String;
				$IndentLength += 1;
			}
			$IndentString = substr ($String, 0, $IndentLength);
			$IndentSpaces = substr ($IndentString, 0, -2);
			if ('' === trim ($IndentSpaces)) {
				$IndentTrail = substr ($IndentString, -1);
				if (' ' === $IndentTrail) {
					$String = substr ($String, $IndentLength);
					$Level = ($IndentLength - 2) / 2;
					$Operator = substr ($IndentString, -2, 1);
				}
			}			
		}
		if ($Level > $this->_SavedIndentLevel and ' ' === $Operator) {
			$Level = $this->_SavedIndentLevel;
			$String = '  '.$String;
		}
		if ($this->CheckPrefix ('#!', $String)) {
			$this->_CodeState = array ('Active' => TRUE, 'WSPrefix' => NULL, 'WSPrefixLen' => 0, 'Level' => $Level, 'Operator' => ' ');
			return (new HeatherLineSpecial ('Code', rtrim (substr ($String, 2)), $Level, $Operator));
		}
		if ($this->CheckPrefix ('-- ', $String)) return (new HeatherLineSpecial ('Dialogue', substr ($String, 3), $Level, $Operator));
		if (($QuoteLevel = strspn ($String, '>')) > 0 and ' ' === substr ($String, $QuoteLevel, 1)) return (new HeatherLineSpecial ('Quote', substr ($String, $QuoteLevel + 1), $Level, $Operator, $QuoteLevel));
		return (new HeatherLine ($String, $Level, $Operator));
	}

	// public interface
	public function __construct ($Source) {
		is_scalar ($Source) and $Source = explode ("\n", str_replace ("\r\n", "\n", $Source));
		is_array ($Source) or trigger_error ('Somehow the source is not an array, not sure what to do.', E_USER_ERROR);
		$this->_Strings = $Source;
		$Count = 0;
		if (count ($this->_Strings) and (self::SIGNATURE === $this->_Strings [$Count])) {
			$Count += 1;
			while (! HeatherString::IsEmpty ($String = $this->_Strings [$Count])) {
				$Instruction = explode (' ', $String, 2);
				$Parameters = (count ($Instruction) > 1) ? $Instruction [1] : '';
				$Instruction = $Instruction [0];
				switch ($Instruction) {
					case 'TITLE':
						strlen ($this->_Head ['Title']) and $this->_Head ['Title'] .= ' ';
						$this->_Head ['Title'] .= trim ($Parameters);
					break;
					default: trigger_error ("Document head includes an unknown instruction [$Instruction]", E_USER_WARNING);
				}
				$Count += 1;
			}
		}
		$this->_Current = $Count - 1;
		$this->_EmptyCounter = 0;
		$this->CodeStateOff ();
	}
	public function GetTitle () {
		array_key_exists ('Title', $this->_Head) or $this->_Head ['Title'] = '';
		return ($this->_Head ['Title']);
	}
	public function NextLine () {
		if ($this->_Current >= (count ($this->_Strings) - 1)) return (NULL);
		$this->_Current += 1;
		$String = $this->_Strings [$this->_Current];
		$Line = $this->ConstructLine ($String);
		$this->_SavedIndentLevel = $Line->Level;
		return ($Line);
	}
	public function CurrentLine () {
		return ($this->_Current + 1);
	}
} ?>